package cn.jingyuan.owl.utils.core.io.file;

import cn.jingyuan.owl.utils.core.StringUtils;
import cn.jingyuan.owl.utils.core.io.FileUtils;
import cn.jingyuan.owl.utils.core.io.IORuntimeException;
import cn.jingyuan.owl.utils.core.lang.Assert;
import cn.jingyuan.owl.utils.core.lang.copier.SrcToDestCopier;

import java.io.File;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;

/**
 * 文件拷贝器<br>
 * 支持以下几种情况：
 * <pre>
 * 1、文件复制到文件
 * 2、文件复制到目录
 * 3、目录复制到目录
 * 4、目录下的文件和目录复制到另一个目录
 * </pre>
 */
public class FileCopier extends SrcToDestCopier<File, FileCopier> {

    /** 是否覆盖目标文件 */
    private boolean isOverride;

    /** 是否拷贝所有属性 */
    private boolean isCopyAttributes;

    /** 当拷贝来源是目录时是否只拷贝目录下的内容 */
    private boolean isCopyContentIfDir;

    /** 当拷贝来源是目录时是否只拷贝文件而忽略子目录 */
    private boolean isOnlyCopyFile;

    /**
     * 构造
     *
     * @param src 源文件
     * @param dest 目标文件
     */
    public FileCopier(File src, File dest) {
        this.src = src;
        this.dest = dest;
    }

    /**
     * 新建一个文件复制器
     *
     * @param srcPath 源文件路径（相对 ClassPath 路径或绝对路径）
     * @param destPath 目标文件路径（相对 ClassPath 路径或绝对路径）
     *
     * @return {@link FileCopier}
     */
    public static FileCopier create(String srcPath, String destPath) {
        return new FileCopier(FileUtils.file(srcPath), FileUtils.file(destPath));
    }

    /**
     * 新建一个文件复制器
     *
     * @param src 源文件
     * @param dest 目标文件
     *
     * @return {@link FileCopier}
     */
    public static FileCopier create(File src, File dest) {
        return new FileCopier(src, dest);
    }

    /**
     * 是否覆盖目标文件
     *
     * @return 是否覆盖目标文件
     */
    public boolean isOverride() {
        return isOverride;
    }

    /**
     * 设置是否覆盖目标文件
     *
     * @param isOverride 是否覆盖目标文件
     *
     * @return this
     */
    public FileCopier setOverride(boolean isOverride) {
        this.isOverride = isOverride;
        return this;
    }

    /**
     * 是否拷贝所有属性
     *
     * @return 是否拷贝所有属性
     */
    public boolean isCopyAttributes() {
        return isCopyAttributes;
    }

    /**
     * 设置是否拷贝所有属性
     *
     * @param isCopyAttributes 是否拷贝所有属性
     *
     * @return this
     */
    public FileCopier setCopyAttributes(boolean isCopyAttributes) {
        this.isCopyAttributes = isCopyAttributes;
        return this;
    }

    /**
     * 当拷贝来源是目录时是否只拷贝目录下的内容
     *
     * @return 当拷贝来源是目录时是否只拷贝目录下的内容
     */
    public boolean isCopyContentIfDir() {
        return isCopyContentIfDir;
    }

    /**
     * 当拷贝来源是目录时是否只拷贝目录下的内容
     *
     * @param isCopyContentIfDir 是否只拷贝目录下的内容
     *
     * @return this
     */
    public FileCopier setCopyContentIfDir(boolean isCopyContentIfDir) {
        this.isCopyContentIfDir = isCopyContentIfDir;
        return this;
    }

    /**
     * 当拷贝来源是目录时是否只拷贝文件而忽略子目录
     *
     * @return 当拷贝来源是目录时是否只拷贝文件而忽略子目录
     */
    public boolean isOnlyCopyFile() {
        return isOnlyCopyFile;
    }

    /**
     * 设置当拷贝来源是目录时是否只拷贝文件而忽略子目录
     *
     * @param isOnlyCopyFile 当拷贝来源是目录时是否只拷贝文件而忽略子目录
     *
     * @return this
     */
    public FileCopier setOnlyCopyFile(boolean isOnlyCopyFile) {
        this.isOnlyCopyFile = isOnlyCopyFile;
        return this;
    }

    /**
     * 执行拷贝<br>
     * 拷贝规则为：
     * <pre>
     * 1、源为文件，目标为已存在目录，则拷贝到目录下，文件名不变
     * 2、源为文件，目标为不存在路径，则目标以文件对待（自动创建父级目录）比如：/dest/aaa，如果 aaa 不存在，则 aaa 被当作文件名
     * 3、源为文件，目标是一个已存在的文件，则当 {@link #setOverride(boolean)} 设为 true 时会被覆盖，默认不覆盖
     * 4、源为目录，目标为已存在目录，当 {@link #setCopyContentIfDir(boolean)} 为 true 时，只拷贝目录中的内容到目标目录中，否则整个源目录连同其目录拷贝到目标目录中
     * 5、源为目录，目标为不存在路径，则自动创建目标为新目录，然后按照规则 4 复制
     * 6、源为目录，目标为文件，抛出 IO 异常
     * 7、源路径和目标路径相同时，抛出 IO 异常
     * </pre>
     *
     * @return 拷贝后目标的文件或目录
     *
     * @throws IORuntimeException IO 异常
     */
    @Override
    public File copy() throws IORuntimeException {
        final File src = this.src;
        final File dest = this.dest;
        // check
        Assert.notNull(src, "Source File is null !");
        if (!src.exists()) {
            throw new IORuntimeException("File not exist: " + src);
        }
        Assert.notNull(dest, "Destination File or directiory is null !");
        if (FileUtils.equals(src, dest)) {
            throw new IORuntimeException("Files '{}' and '{}' are equal", src, dest);
        }

        // 复制目录
        if (src.isDirectory()) {
            if (dest.exists() && !dest.isDirectory()) {
                // 源为目录，目标为文件，抛出 IO 异常
                throw new IORuntimeException("Src is a directory but dest is a file!");
            }
            if (FileUtils.isSub(src, dest)) {
                throw new IORuntimeException("Dest is a sub directory of src !");
            }

            final File subDest = isCopyContentIfDir ? dest : FileUtils.mkdir(FileUtils.file(dest, src.getName()));
            internalCopyDirContent(src, subDest);
        } else {// 复制文件
            internalCopyFile(src, dest);
        }
        return dest;
    }

    /**
     * 拷贝目录内容，只用于内部，不做任何安全检查<br>
     * 拷贝内容的意思为源目录下的所有文件和目录拷贝到另一个目录下，而不拷贝源目录本身
     *
     * @param src 源目录
     * @param dest 目标目录
     *
     * @throws IORuntimeException IO 异常
     */
    private void internalCopyDirContent(File src, File dest) throws IORuntimeException {
        if (null != copyFilter && !copyFilter.accept(src)) {
            // 被过滤的目录跳过
            return;
        }

        if (!dest.exists()) {
            // 目标为不存在路径，创建为目录
            dest.mkdirs();
        } else if (!dest.isDirectory()) {
            throw new IORuntimeException(StringUtils.format("Src [{}] is a directory but dest [{}] is a file!", src.getPath(), dest.getPath()));
        }

        final String[] files = src.list();
        File srcFile;
        File destFile;
        for (String file : files) {
            srcFile = new File(src, file);
            destFile = this.isOnlyCopyFile ? dest : new File(dest, file);
            // 递归复制
            if (srcFile.isDirectory()) {
                internalCopyDirContent(srcFile, destFile);
            } else {
                internalCopyFile(srcFile, destFile);
            }
        }
    }

    /**
     * 拷贝文件，只用于内部，不做任何安全检查<br>
     * 情况如下：
     * <pre>
     * 1、如果目标是一个不存在的路径，则目标以文件对待（自动创建父级目录）比如：/dest/aaa，如果 aaa 不存在，则 aaa 被当作文件名
     * 2、如果目标是一个已存在的目录，则文件拷贝到此目录下，文件名与原文件名一致
     * </pre>
     *
     * @param src 源文件，必须为文件
     * @param dest 目标文件，如果非覆盖模式必须为目录
     *
     * @throws IORuntimeException IO 异常
     */
    private void internalCopyFile(File src, File dest) throws IORuntimeException {
        if (null != copyFilter && !copyFilter.accept(src)) {
            // 被过滤的文件跳过
            return;
        }

        // 如果已经存在目标文件，切为不覆盖模式，跳过之
        if (dest.exists()) {
            if (dest.isDirectory()) {
                // 目标为目录，目录下创建同名文件
                dest = new File(dest, src.getName());
            }

            if (dest.exists() && !isOverride) {
                // 非覆盖模式跳过
                return;
            }
        } else {
            // 路径不存在则创建父目录
            dest.getParentFile().mkdirs();
        }

        final ArrayList<CopyOption> optionList = new ArrayList<>(2);
        if (isOverride) {
            optionList.add(StandardCopyOption.REPLACE_EXISTING);
        }
        if (isCopyAttributes) {
            optionList.add(StandardCopyOption.COPY_ATTRIBUTES);
        }

        try {
            Files.copy(src.toPath(), dest.toPath(), optionList.toArray(new CopyOption[optionList.size()]));
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

}
